﻿// <file>
//     <copyright see="prj:///doc/copyright.txt"/>
//     <license see="prj:///doc/license.txt"/>
//     <owner name="Mike Krüger" email="mike@icsharpcode.net"/>
//     <version>$Revision$</version>
// </file>

using System;
using System.Drawing;
using System.Diagnostics;
using System.Windows.Forms;
using ICSharpCode.TextEditor.Document;

namespace ICSharpCode.TextEditor.Gui.CompletionWindow
{
    public class CodeCompletionWindow : AbstractCompletionWindow
    {
        Form ownerForm = null;
        string fileName = "";
        ICompletionData[] completionData;
        CodeCompletionListView codeCompletionListView;
        VScrollBar vScrollBar = new VScrollBar();
        ICompletionDataProvider dataProvider;
        IDocument document;
        bool showDeclarationWindow = true;
        bool fixedListViewWidth = true;
        const int ScrollbarWidth = 16;
        const int MaxListLength = 10;

        int startOffset;
        int endOffset;
        DeclarationViewWindow declarationViewWindow = null;
        static CodeCompletionWindow codeCompletionWindow = null;
        Rectangle workingScreen;

        public static CodeCompletionWindow ShowCompletionWindow(Form parent, TextEditorControl control, string fileName, ICompletionDataProvider completionDataProvider, char firstChar)
        {
            return ShowCompletionWindow(parent, control, fileName, completionDataProvider, firstChar, true, true);
        }

        public static CodeCompletionWindow ShowCompletionWindow(Form parent, TextEditorControl control, string fileName, ICompletionDataProvider completionDataProvider, char firstChar, bool showDeclarationWindow, bool fixedListViewWidth)
        {
            ICompletionData[] completionData = completionDataProvider.GenerateCompletionData(fileName, control.ActiveTextAreaControl.TextArea, firstChar);
            if (completionData == null || completionData.Length == 0)
            {
                if (codeCompletionWindow != null)
                {
                    codeCompletionWindow.CloseWhenCaretAtBeginning = true;
                    codeCompletionWindow.Close();
                }
                return null;
            }
            codeCompletionWindow = new CodeCompletionWindow(completionDataProvider, completionData, fileName, parent, control, showDeclarationWindow, fixedListViewWidth);
            codeCompletionWindow.CloseWhenCaretAtBeginning = firstChar == '\0';
            codeCompletionWindow.ShowCompletionWindow();
            return codeCompletionWindow;
        }

        public void ResetCompletionData(ICompletionDataProvider completionDataProvider, TextAreaControl textAreaControl, char firstChar)
        {
            this.dataProvider = completionDataProvider;

            completionData = completionDataProvider.GenerateCompletionData(fileName, textAreaControl.TextArea, firstChar);
            if (completionData == null || completionData.Length == 0)
            {
                if (codeCompletionWindow != null)
                {
                    codeCompletionWindow.CloseWhenCaretAtBeginning = true;
                    codeCompletionWindow.Close();
                }
            }
            else
            {
                UpdateCompletionListView();
                codeCompletionWindow.CloseWhenCaretAtBeginning = firstChar == '\0';
                codeCompletionWindow.ShowCompletionWindow();
            }
        }

        CodeCompletionWindow(ICompletionDataProvider completionDataProvider, ICompletionData[] completionData, string fileName, Form parentForm, TextEditorControl control, bool showDeclarationWindow, bool fixedListViewWidth) : base(parentForm, control)
        {
            this.fileName = fileName;
            this.ownerForm = parentForm;
            this.dataProvider = completionDataProvider;
            this.completionData = completionData;
            this.document = control.Document;
            this.showDeclarationWindow = showDeclarationWindow;
            this.fixedListViewWidth = fixedListViewWidth;
            CreateCompletionListView();
            document.DocumentAboutToBeChanged += DocumentAboutToBeChanged;
        }

        bool inScrollUpdate;

        bool ResetScrollBar()
        {
            bool scrollBarVisable = false;
            if (completionData.Length > MaxListLength)
            {
                vScrollBar.Dock = DockStyle.Right;
                vScrollBar.Minimum = 0;
                vScrollBar.Maximum = completionData.Length - 1;
                vScrollBar.SmallChange = 1;
                vScrollBar.LargeChange = MaxListLength;
                scrollBarVisable = true;

                Controls.Add(vScrollBar);
                vScrollBar.ValueChanged += VScrollBarValueChanged;
                codeCompletionListView.FirstItemChanged += new EventHandler(CodeCompletionListViewFirstItemChanged);

            }
            else
            {
                Controls.Remove(vScrollBar);
                vScrollBar.ValueChanged -= VScrollBarValueChanged;
                codeCompletionListView.FirstItemChanged -= new EventHandler(CodeCompletionListViewFirstItemChanged);
            }
            return scrollBarVisable;
        }

        void UpdateCompletionListView()
        {
            showDeclarationWindow = false;

            var completionDataProvider = this.dataProvider;

            workingScreen = Screen.GetWorkingArea(Location);
            startOffset = control.ActiveTextAreaControl.Caret.Offset + 1;
            endOffset = startOffset;
            if (completionDataProvider.PreSelection != null)
            {
                startOffset -= completionDataProvider.PreSelection.Length + 1;
                endOffset--;
            }

            codeCompletionListView.CompletionData = completionData;

            ResetScrollBar();

            this.drawingSize = GetListViewSize();
            SetLocation();

            CodeCompletionListViewSelectedItemChanged(this, EventArgs.Empty);

            if (completionDataProvider.DefaultIndex >= 0)
            {
                codeCompletionListView.SelectIndex(completionDataProvider.DefaultIndex);
            }
            else
            {
                codeCompletionListView.SelectIndex(0);
            }

            control.Focus();

            if (declarationViewWindow == null)
            {
                declarationViewWindow = new DeclarationViewWindow(ownerForm);
                declarationViewWindow.MouseMove += ControlMouseMove;
            }
            SetDeclarationViewLocation();
            if (showDeclarationWindow)
            {
                declarationViewWindow.ShowDeclarationViewWindow();
            }

            codeCompletionListView.Focus();

            if (completionDataProvider.PreSelection != null)
            {
                CaretOffsetChanged(this, EventArgs.Empty);
            }

            showDeclarationWindow = true;
        }

        void CreateCompletionListView()
        {
            var completionDataProvider = this.dataProvider;

            workingScreen = Screen.GetWorkingArea(Location);
            startOffset = control.ActiveTextAreaControl.Caret.Offset + 1;
            endOffset = startOffset;
            if (completionDataProvider.PreSelection != null)
            {
                startOffset -= completionDataProvider.PreSelection.Length + 1;
                endOffset--;
            }

            codeCompletionListView = new CodeCompletionListView(completionData);
            codeCompletionListView.ImageList = completionDataProvider.ImageList;
            codeCompletionListView.Dock = DockStyle.Fill;
            codeCompletionListView.Theme = "Default";
            codeCompletionListView.SelectedItemChanged += new EventHandler(CodeCompletionListViewSelectedItemChanged);
            codeCompletionListView.DoubleClick += new EventHandler(CodeCompletionListViewDoubleClick);
            codeCompletionListView.Click += new EventHandler(CodeCompletionListViewClick);
            Controls.Add(codeCompletionListView);

            ResetScrollBar();

            this.drawingSize = GetListViewSize();
            SetLocation();

            if (declarationViewWindow == null)
            {
                declarationViewWindow = new DeclarationViewWindow(ownerForm);
                declarationViewWindow.MouseMove += ControlMouseMove;
            }
            SetDeclarationViewLocation();
            if (showDeclarationWindow)
            {
                declarationViewWindow.ShowDeclarationViewWindow();
            }
            control.Focus();
            CodeCompletionListViewSelectedItemChanged(this, EventArgs.Empty);

            if (completionDataProvider.DefaultIndex >= 0)
            {
                codeCompletionListView.SelectIndex(completionDataProvider.DefaultIndex);
            }
            else
            {
                codeCompletionListView.SelectIndex(0);
            }
            codeCompletionListView.Focus();

            if (completionDataProvider.PreSelection != null)
            {
                CaretOffsetChanged(this, EventArgs.Empty);
            }
        }

        void CodeCompletionListViewFirstItemChanged(object sender, EventArgs e)
        {
            if (inScrollUpdate) return;
            inScrollUpdate = true;
            vScrollBar.Value = Math.Min(vScrollBar.Maximum, codeCompletionListView.FirstItem);
            inScrollUpdate = false;
        }

        void VScrollBarValueChanged(object sender, EventArgs e)
        {
            if (inScrollUpdate)
                return;
            inScrollUpdate = true;
            codeCompletionListView.FirstItem = vScrollBar.Value;
            codeCompletionListView.Refresh();
            control.ActiveTextAreaControl.TextArea.Focus();
            inScrollUpdate = false;
        }

        void SetDeclarationViewLocation()
        {
            //  This method uses the side with more free space
            int leftSpace = Bounds.Left - workingScreen.Left;
            int rightSpace = workingScreen.Right - Bounds.Right;
            Point pos;
            // The declaration view window has better line break when used on
            // the right side, so prefer the right side to the left.
            if (rightSpace * 2 > leftSpace)
            {
                declarationViewWindow.FixedWidth = false;
                pos = new Point(Bounds.Right, Bounds.Top);
                if (declarationViewWindow.Location != pos)
                {
                    declarationViewWindow.Location = pos;
                }
            }
            else
            {
                declarationViewWindow.Width = declarationViewWindow.GetRequiredLeftHandSideWidth(new Point(Bounds.Left, Bounds.Top));
                declarationViewWindow.FixedWidth = true;
                if (Bounds.Left < declarationViewWindow.Width)
                {
                    pos = new Point(0, Bounds.Top);
                }
                else
                {
                    pos = new Point(Bounds.Left - declarationViewWindow.Width, Bounds.Top);
                }
                if (declarationViewWindow.Location != pos)
                {
                    declarationViewWindow.Location = pos;
                }
                declarationViewWindow.Refresh();
            }
        }

        protected override void SetLocation()
        {
            base.SetLocation();
            if (declarationViewWindow != null)
            {
                SetDeclarationViewLocation();
            }
        }

        ICSharpCode.TextEditor.Util.MouseWheelHandler mouseWheelHandler = new ICSharpCode.TextEditor.Util.MouseWheelHandler();

        public void HandleMouseWheel(MouseEventArgs e)
        {
            int scrollDistance = mouseWheelHandler.GetWheelDeltaScrollDistance(e);
            if (scrollDistance == 0)
                return;
            if (control.TextEditorProperties.MouseWheelScrollDown)
                scrollDistance = -scrollDistance;
            int newValue = vScrollBar.Value + vScrollBar.SmallChange * scrollDistance;
            vScrollBar.Value = Math.Max(vScrollBar.Minimum, Math.Min(vScrollBar.Maximum - vScrollBar.LargeChange + 1, newValue));
        }

        void CodeCompletionListViewSelectedItemChanged(object sender, EventArgs e)
        {
            ICompletionData data = codeCompletionListView.SelectedCompletionData;
            if (showDeclarationWindow && data != null && data.Description != null && data.Description.Length > 0)
            {
                declarationViewWindow.Description = data.Description;
                SetDeclarationViewLocation();
            }
            else
            {
                declarationViewWindow.Description = null;
            }
        }

        public override bool ProcessKeyEvent(char ch)
        {
            switch (dataProvider.ProcessKey(ch))
            {
                case CompletionDataProviderKeyResult.BeforeStartKey:
                    // increment start+end, then process as normal char
                    ++startOffset;
                    ++endOffset;
                    return base.ProcessKeyEvent(ch);
                case CompletionDataProviderKeyResult.NormalKey:
                    // just process normally
                    return base.ProcessKeyEvent(ch);
                case CompletionDataProviderKeyResult.InsertionKey:
                    return InsertSelectedItem(ch);
                default:
                    throw new InvalidOperationException("Invalid return value of dataProvider.ProcessKey");
            }
        }

        void DocumentAboutToBeChanged(object sender, DocumentEventArgs e)
        {
            // => startOffset test required so that this startOffset/endOffset are not incremented again
            //    for BeforeStartKey characters
            if (e.Offset >= startOffset && e.Offset <= endOffset)
            {
                if (e.Length > 0)
                { // length of removed region
                    endOffset -= e.Length;
                }
                if (!string.IsNullOrEmpty(e.Text))
                {
                    endOffset += e.Text.Length;
                }
            }
        }

        /// <summary>
        /// When this flag is set, code completion closes if the caret moves to the
        /// beginning of the allowed range. This is useful in Ctrl+Space and "complete when typing",
        /// but not in dot-completion.
        /// </summary>
        public bool CloseWhenCaretAtBeginning { get; set; }

        protected override void CaretOffsetChanged(object sender, EventArgs e)
        {
            int offset = control.ActiveTextAreaControl.Caret.Offset;
            if (offset == startOffset)
            {
                if (CloseWhenCaretAtBeginning)
                    Close();
                return;
            }
            if (offset < startOffset || offset > endOffset)
            {
                Close();
            }
            else
            {
                codeCompletionListView.SelectItemWithStart(control.Document.GetText(startOffset, offset - startOffset));
            }
        }

        protected override bool ProcessTextAreaKey(Keys keyData)
        {
            if (!Visible)
            {
                return false;
            }

            switch (keyData)
            {
                case Keys.Home:
                    codeCompletionListView.SelectIndex(0);
                    return true;
                case Keys.End:
                    codeCompletionListView.SelectIndex(completionData.Length - 1);
                    return true;
                case Keys.PageDown:
                    codeCompletionListView.PageDown();
                    return true;
                case Keys.PageUp:
                    codeCompletionListView.PageUp();
                    return true;
                case Keys.Down:
                    codeCompletionListView.SelectNextItem();
                    return true;
                case Keys.Up:
                    codeCompletionListView.SelectPrevItem();
                    return true;
                case Keys.Tab:
                    InsertSelectedItem('\t');
                    return true;
                case Keys.Return:
                    InsertSelectedItem('\n');
                    return true;
            }
            return base.ProcessTextAreaKey(keyData);
        }

        void CodeCompletionListViewDoubleClick(object sender, EventArgs e)
        {
            InsertSelectedItem('\0');
        }

        void CodeCompletionListViewClick(object sender, EventArgs e)
        {
            control.ActiveTextAreaControl.TextArea.Focus();
        }

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                vScrollBar.ValueChanged -= VScrollBarValueChanged;
                document.DocumentAboutToBeChanged -= DocumentAboutToBeChanged;
                if (codeCompletionListView != null)
                {
                    codeCompletionListView.SelectedItemChanged -= new EventHandler(CodeCompletionListViewSelectedItemChanged);
                    codeCompletionListView.DoubleClick -= new EventHandler(CodeCompletionListViewDoubleClick);
                    codeCompletionListView.Click -= new EventHandler(CodeCompletionListViewClick);
                    codeCompletionListView.Close();
                    codeCompletionListView.Dispose();
                    codeCompletionListView = null;
                }
                if (declarationViewWindow != null)
                {
                    declarationViewWindow.MouseMove -= ControlMouseMove;
                    declarationViewWindow.Close();
                    declarationViewWindow.Dispose();
                    declarationViewWindow = null;
                }
            }
            base.Dispose(disposing);
        }

        bool InsertSelectedItem(char ch)
        {
            document.DocumentAboutToBeChanged -= DocumentAboutToBeChanged;
            ICompletionData data = codeCompletionListView.SelectedCompletionData;
            bool result = false;
            if (data != null)
            {
                control.BeginUpdate();

                try
                {
                    if (endOffset - startOffset > 0)
                    {
                        control.Document.Remove(startOffset, endOffset - startOffset);
                    }
                    Debug.Assert(startOffset <= document.TextLength);
                    result = dataProvider.InsertAction(data, control.ActiveTextAreaControl.TextArea, startOffset, ch);
                }
                finally
                {
                    control.EndUpdate();
                }
            }
            Close();
            return result;
        }

        Size GetListViewSize()
        {
            int height = codeCompletionListView.ItemHeight * Math.Min(MaxListLength, completionData.Length) + 2;
            int width = codeCompletionListView.ItemHeight * 10;
            if (!fixedListViewWidth)
            {
                width = GetListViewWidth(width, height);
            }
            return new Size(width, height);
        }

        /// <summary>
        /// Gets the list view width large enough to handle the longest completion data
        /// text string.
        /// </summary>
        /// <param name="defaultWidth">The default width of the list view.</param>
        /// <param name="height">The height of the list view.  This is
        /// used to determine if the scrollbar is visible.</param>
        /// <returns>The list view width to accommodate the longest completion
        /// data text string; otherwise the default width.</returns>
        int GetListViewWidth(int defaultWidth, int height)
        {
            float width = defaultWidth;
            using (Graphics graphics = codeCompletionListView.CreateGraphics())
            {
                for (int i = 0; i < completionData.Length; ++i)
                {
                    float itemWidth = graphics.MeasureString(completionData[i].Text.ToString(), codeCompletionListView.Font).Width;
                    if (itemWidth > width)
                    {
                        width = itemWidth;
                    }
                }
            }

            float totalItemsHeight = codeCompletionListView.ItemHeight * completionData.Length;
            if (totalItemsHeight > height)
            {
                width += ScrollbarWidth; // Compensate for scroll bar.
            }
            return (int)width;
        }
    }
}
